import sys, os

from twisted.internet import wxreactor
try:
    wxreactor.install()
except:
    pass

import agversion
agversion.select(3)
from AccessGrid.Platform.Config import UserConfig
from AccessGrid.Toolkit import WXGUIApplication
from AccessGrid.SharedAppClient import SharedAppClient
from AccessGrid.DataStoreClient import GetVenueDataStore
from AccessGrid.VenueClient import GetVenueClientUrls
from AccessGrid.interfaces.VenueClient_client import VenueClientIW

#sys.path.insert(0, os.path.join(UserConfig.instance().GetBaseDir(),
#                                'SharedApplications', 'Shared_OSG') )

import traceback
import cPickle
import threading
import time
import string
import tempfile
import ConfigParser

import wx

from MainFrame import MainFrame
from SubController import SubController
from OSGFrame import OSGFrame
import Constants

import osg, osgDB



class MainController:
    """
        Main class of the program.
        This class is executed as a script from within Access Grid.
    """
    
    maincontroller = None
    
    def __init__( self, appUrl, venueUrl ):
        """
            Parameters:
                appUrl: URL of the Shared Application
                venueUrl: URL of the Access Grid Venue
        """
        
        ##############################
        # start: list of all members #
        ##############################
        
        self.mainframe = None
        
        self.appUrl = appUrl
        self.venueUrl = venueUrl
        self.dataStoreClient = None
        
        self.sharedAppClient = SharedAppClient( 'SharedOSG' )
        self.log = self.sharedAppClient.InitLogging() # We don't really need this, but if we remove it, self.sharedAppClient.Shutdown() will not work properly.
        self.clientProfile = None
        self.publicId = None
        
        # Dictionary which holds the data about the participants
        # (display name, pointer shape, pointer color, node for the pointer).
        # The keys are the public IDs of the users.
        # The value of a key (i.e. the data of one participant) is also a dictionary.
        # Example:
        # {
        #  '84b5f7dd08d02ddf543f748fe77ca0c0': 
        #        {'telepointer2dColor': 'orange', 'telepointer2dShape': 'triangle', 'telepointer2dNode': <osg.Node; proxy of <Swig Object of type 'osg::Node *' at 0x3566170> >, 'name': 'Florian'}, 
        #  '84b5f77904e86ab6d3ae77fe329ab105': 
        #        {'telepointer2dColor': 'white', 'telepointer2dShape': 'circle', 'telepointer2dNode': <osg.Node; proxy of <Swig Object of type 'osg::Node *' at 0x37357c8> >, 'name': 'HIT Lab Test'}
        # }
        self.participantsData = {}
        
        self.subcontrollers = {}
        self.shutdown = False
        
        # In this list the names of all the downloaded files that stored.
        # When the program is shut down these must be deleted.
        self.tempfiles = []
        
        ############################
        # end: list of all members #
        ############################
        
        
        # get the client profile
        venueClientUrl = GetVenueClientUrls()[0]
        venueClient = VenueClientIW(venueClientUrl)
        self.clientProfile = venueClient.GetClientProfile()
        self.publicId = str( self.clientProfile.GetPublicId() )
        
        # load the data from the config file
        self.LoadDataFromConfigFile( str(self.clientProfile.GetName()) )
        
        # get the data store client of the venue
        self.dataStoreClient = GetVenueDataStore(venueUrl, self.clientProfile.connectionId)

        # join the shared application
        self.sharedAppClient.Join( self.appUrl, self.clientProfile )
        
        # register callback functions
        self.sharedAppClient.RegisterEventCallback( 'OpenFileFromDatastore', self.OpenFileFromDatastoreEvent )
        self.sharedAppClient.RegisterEventCallback( 'ParticipantJoin', self.ParticipantJoinEvent )

        # create main window
        self.mainframe = MainFrame( self )
        self.mainframe.Show()
        
        # to make sure that no events are sent before a connection is made
        self.sharedAppClient.eventClient.RegisterMadeConnectionCallback( self.ConnectionMade )
        
        # to be able to tell the user when the connection was lost
        self.sharedAppClient.eventClient.RegisterLostConnectionCallback( self.ConnectionLost )
        
        
    def LoadDataFromConfigFile(self, name):
        """
            Loads the data for the local user from the config file 
            (such as displayed name or telepointer style).
            Parameters:
                name: If there is no name specified in the config file
                    this name will be used.
        """
        config = ConfigParser.RawConfigParser()
        config.read('config.ini')
            
        nameFromConfig = config.get('main', 'name')
        if nameFromConfig != '':
            name = nameFromConfig
        telepointer2dShape = config.get('telepointer2d', 'shape')
        telepointer2dColor = config.get('telepointer2d', 'color')
        
        telepointer2dNode = self.CreateTelepointer2dNode( telepointer2dShape, telepointer2dColor )
        
        self.participantsData[self.publicId] = {
                'name': name, 
                'telepointer2dShape': telepointer2dShape, 
                'telepointer2dColor': telepointer2dColor, 
                'telepointer2dNode': telepointer2dNode 
                }
    
    
    def CreateTelepointer2dNode(self, shape, color):
        """
            Creates an OpenSceneGraph node for the 2D telepointer mode.
            Parameters:
                shape: Shape of the telepointer (circle, square, triangle).
                color: Color of the telepointer (white, blue, yellow, red, orange,
                    green, purple, black).
        """
        
        geode = osg.Geode()
        geom = osg.Geometry()
        coords = osg.Vec3Array()
        geom.setVertexArray( coords )
        colors = osg.Vec4Array()
        geom.setColorBinding(osg.Geometry.BIND_OVERALL)
        geom.setColorArray(colors)
        
        if shape == 'square':
            coords.push_back( osg.Vec3(-8, -8, 0) )
            coords.push_back( osg.Vec3(8, -8, 0) )
            coords.push_back( osg.Vec3(8, 8, 0) )
            coords.push_back( osg.Vec3(-8, 8, 0) )
            geom.addPrimitiveSet( osg.DrawArrays( osg.GL_QUADS, 0, coords.getNumElements() ) )
        elif shape == 'circle':
            coords.push_back( osg.Vec3(8, 0, 0) )
            coords.push_back( osg.Vec3(8, 4, 0) )
            coords.push_back( osg.Vec3(4, 8, 0) )
            coords.push_back( osg.Vec3(0, 8, 0) )
            coords.push_back( osg.Vec3(-4, 8, 0) )
            coords.push_back( osg.Vec3(-8, 4, 0) )
            coords.push_back( osg.Vec3(-8, 0, 0) )
            coords.push_back( osg.Vec3(-8, -4, 0) )
            coords.push_back( osg.Vec3(-4, -8, 0) )
            coords.push_back( osg.Vec3(0, -8, 0) )
            coords.push_back( osg.Vec3(4, -8, 0) )
            coords.push_back( osg.Vec3(8, -4, 0) )
            geom.addPrimitiveSet( osg.DrawArrays( osg.GL_POLYGON, 0, coords.getNumElements() ) )
        elif shape == 'triangle':
            coords.push_back( osg.Vec3(8, -8, 0) )
            coords.push_back( osg.Vec3(0, 8, 0) )
            coords.push_back( osg.Vec3(-8, -8, 0) )
            geom.addPrimitiveSet( osg.DrawArrays( osg.GL_TRIANGLES, 0, coords.getNumElements() ) )
        
        if color == 'white':
            colors.push_back( osg.Vec4(1, 1, 1, 1) )
        elif color == 'red':
            colors.push_back( osg.Vec4(1, 0, 0, 1) )
        elif color == 'blue':
            colors.push_back( osg.Vec4(0, 0, 1, 1) )
        elif color == 'yellow':
            colors.push_back( osg.Vec4(1, 1, 0, 1) )
        elif color == 'orange':
            colors.push_back( osg.Vec4(1, 0.64, 0, 1) )
        elif color == 'green':
            colors.push_back( osg.Vec4(0, 1, 0, 1) )
        elif color == 'purple':
            colors.push_back( osg.Vec4(1, 0, 1, 1) )
        elif color == 'black':
            colors.push_back( osg.Vec4(0, 0, 0, 1) )
            
        geode.addDrawable( geom )
        
#        # text not supported yet
#        import osgText
#        text = osgText.Text()
#        text.setText('This is an example sentence.')
#        text.setCharacterSize(50)
#        
#        geode.addDrawable(text)
        
        
        osgDB.writeNodeFile( geode, shape + color + '.osg' )
        cursorNode = osgDB.readNodeFile( shape + color + '.osg' )
        return cursorNode
    
    
    def ConnectionMade(self):
        """ 
            This method will be called from the AccessGrid toolkit as soon as the connection
            to the event channel has been established.
            As soon as this happens the application really starts and all the
            OSG viewer frames from the session are opened.
        """
        
        # Store the data from the local user on the server.
        # Only the first 3 values are stored, not the 4th one which is the osg node.
        
        # Remove osg node from the dictionary
        telepointer2dNode = self.participantsData[self.publicId].pop('telepointer2dNode')
        
        # Put the data about the local user onto the venue
        self.sharedAppClient.SetData( 'userdata_' + self.publicId,
                                     cPickle.dumps(self.participantsData[self.publicId]) )
        
        # Put the osg node back into the dictionary
        self.participantsData[self.publicId]['telepointer2dNode'] = telepointer2dNode
        
        
        # Notify all other participants that we joined the session
        self.sharedAppClient.SendEvent( 'ParticipantJoin', self.publicId )
        
        
        # Call self.UpdateParticipantsList() once so that the name of the local user
        # is in the displayed participants list. If we leave this out the local user
        # will not be recognised as a new user by the self.UpdateParticipants() method.
        self.UpdateParticipantsList()
        
        # Look up which files are opened in this session and load all of them
        pickledData = self.sharedAppClient.GetData( 'ActiveFrames' )
        if pickledData != '':
            activeFiles = cPickle.loads( str(pickledData) )
            for filenumber in activeFiles:
                filename = self.sharedAppClient.GetData( 'File' + str(filenumber) )
                if filename != '':
                    self.StartUpViewer( filenumber, self.DownloadFileFromVenueDataStore(filename) )
        
        # Start the thread for updating the displayed data.
        timer = threading.Timer( 1, self.DisplayedDataUpdateLoop )
        timer.start()
        

    def ConnectionLost(self, connector, reason):
        """
            This method is called, when the connection to the Shared Application Session
            was lost.
        """
        
        # if the mainframe is still shown the connection was lost accidentally,
        # thus the program must be terminated
        if self.mainframe != None:
            wx.MessageBox( 'The connection was lost.\nThe program will be closed now.', 
                           'No Connection', wx.OK | wx.ICON_EXCLAMATION )
            self.Shutdown()
            
        
    def ParticipantJoinEvent(self, event):
        """
            This method is called via callback when a new participant joins the session.
        """
        
        # The lists of participants (in the GUI and in the program data) must be updated.
        self.UpdateParticipants()
        
        # The telepointer node for this new participant must be 
        # added to every open OSGFrame window.
        participantId = str(event.data)
        for subcontroller in self.subcontrollers:
            subcontroller.AddNodeTo2dTelepointerNode( 
                    participantId, self.participantsData[participantId]['telepointer2dNode'] )
        
        
    def UpdateParticipants(self):
        """
            This method refreshes the data that is collected about the participants
            (e.g. name, telepointer shape).
            If a new participant is found, the function to update the participant list
            in the GUI is called.
        """
        newParticipantFound = False
        participantDescriptions = self.sharedAppClient.GetParticipants()
        for participant in participantDescriptions:
            participantId = str( participant.clientProfile.GetPublicId() )
            
            # If we have a new participant then add his data.
            if not participantId in self.participantsData.keys():
                newParticipantFound = True
                participantData = cPickle.loads( str( self.sharedAppClient.GetData( 'userdata_' + \
                                                 participantId ) ) )
                telepointer2dNode = self.CreateTelepointer2dNode( 
                        participantData['telepointer2dShape'], 
                        participantData['telepointer2dColor'] 
                        )
                participantData['telepointer2dNode'] = telepointer2dNode
                self.participantsData[participantId] = participantData
                
                # Add the telepointer node of the new participant to all the
                # subcontrollers (for every OSGFrame there is one subcontroller),
                # so that the new telepointer node can be displayed in every window.
                for subcontroller in self.subcontrollers.values():
                    subcontroller.AddNodeTo2dTelepointerNode( 
                            participantId, telepointer2dNode )
                
        # If a new participant was found update the list 
        # which shows the names of the participants.
        if newParticipantFound:
            self.UpdateParticipantsList()
        
    def UpdateParticipantsList(self):
        """
            This method refreshes the list of participants in the GUI according
            to the dictionary that stores the data about all participants.
        """
        participants = []
        for participantData in self.participantsData.values():
            participants.append( participantData['name'] )
        self.mainframe.lstParticipants.Clear()
        self.mainframe.lstParticipants.AppendItems(participants)
        
    def DisplayedDataUpdateLoop(self):
        """
            This method does an infinite loop while the main program is running to
            update all the data that is displayed on the GUI.
        """
        while not self.shutdown:
            try:
                print 'updating the GUI'
                
                # Update the file list
                files = self.dataStoreClient.QueryMatchingFilesMultiple(
                                    Constants.SUPPORTED_FILE_EXTENSIONS )
                files.sort()
                files.insert(0, Constants.STR_FILE_LIST_FIRST_ITEM)
                selectedFile = self.mainframe.chFilesOnVenue.GetStringSelection()
                self.mainframe.chFilesOnVenue.Clear()
                self.mainframe.chFilesOnVenue.AppendItems(files)
                self.mainframe.chFilesOnVenue.SetStringSelection(selectedFile)
                self.mainframe.chFilesOnVenue.SetFocus()

                # Update the participants
                self.UpdateParticipants()
                
                # Wait a bit for the next update
                time.sleep(60)
                
            except Exception, inst:
                print "Unexpected error in update display data timer (" + \
                        str( sys.exc_info()[0] ) + '):', sys.exc_info()[1]
                traceback.print_tb(sys.exc_info()[2], None, sys.stdout)


    def OnMainframeDestroy(self, event):
        """
            This method is called when the MainFrame receives the
            wx.EVT_WINDOW_DESTROY event.
            It triggers the shutdown of the program.
        """
        window = event.GetWindow()
        
        # If the destroyed window wasn't the main frame do nothing.
        if window != self.mainframe:
            print 'destroy event didnt come from main frame'
            return
        
        self.mainframe = None
        self.Shutdown()
        
        
    def Shutdown(self):
        """
            This method shuts down the SharedAppClient and
            destroys the MainFrame (if not already destroyed). 
        """
        
        # This is needed so that the GUI update loop will terminate.
        self.shutdown = True
        
        self.sharedAppClient.Shutdown()
        
        # If the mainframe is still showing it has to be destroyed.
        if self.mainframe != None:
            self.mainframe.Destroy()
            
        # Delete all the downloaded files.
        for file in self.tempfiles:
            os.remove(file)
        
    
    def OpenFileFromDatastore(self, filename):
        """
            This method is called to initialize the creation of a new OSGFrame
            with a model file from the venue's data store.
            It acquires a lock for the session, updates the numbers for the OSGFrames
            on the venue and sends an event that makes all participants download
            the model file and start up a new OSGFrame.
            
            Parameters:
                filename: The filename of the model to load.
        """
        
        # Get the lock for this session so that no one else can manipulate the sensitive data.
        self.GetLockForConfereceSession()
        
        # Find out the number for the next OSGFrame.
        currentNumber = self.sharedAppClient.GetData('LastUsedFramenumber')
        if currentNumber != '':
            currentNumber = int(currentNumber) + 1
        else:  # If yet no number was used for this session: start with 0
            currentNumber = 0
            
        self.sharedAppClient.SetData('LastUsedFramenumber', currentNumber)
            
        # Update the venue with this new frame number.
        framelist = self.sharedAppClient.GetData('ActiveFrames')
        if framelist == '':
            framelist = [currentNumber]
        else:
            framelist = cPickle.loads( str(framelist) )
            framelist.append(currentNumber)
        self.sharedAppClient.SetData( 'ActiveFrames', cPickle.dumps(framelist) )
        self.sharedAppClient.SetData( 'File' + str(currentNumber), filename )
        
        # Release the lock.
        self.ReleaseLockForConfereceSession()
        
        # Send an event so that all participants can download the new file
        self.sharedAppClient.SendEvent( 'OpenFileFromDatastore', 
                                        cPickle.dumps( (currentNumber, filename) ) 
                                        )
    
    
    def OpenFileFromDatastoreEvent(self, event):
        """
            This is a callback function that is called when the OpenFileFromDatastore event
            is received.
            It triggers the startup of a new OSGFrame.
        """
        print 'received OpenFileFromDatastoreEvent'
        filenumber, filename = cPickle.loads( str(event.data) )
        
        self.StartUpViewer(filenumber, self.DownloadFileFromVenueDataStore(filename) )
        
        
    def DownloadFileFromVenueDataStore(self, filename):
        """
            This method downloads a model file from the venue's data store and
            stores it locally in a temporary file, the name of which will be returned.
            
            Parameters:
                filename: The filename of the model on the venue's data store.
            Returns: The local filename for the downloaded model.
        """
        
        # Create a temporary file.
        t = string.split(filename, '.')
        ext = ''
        if len(t) > 1:
            ext = '.' + t[len(t)-1]
        (fd, tempname) = tempfile.mkstemp(suffix = ext)
        os.close(fd)

        # Download the model file.
        dsc = GetVenueDataStore( self.venueUrl, self.clientProfile.connectionId )
        dsc.Download( filename, tempname )
        
        # Remember the name of temporary file so that we can delete it again later.
        self.tempfiles.append(tempname)
        
        return tempname
        
        
    def GetLockForConfereceSession(self):
        """
            This method acquires a lock for the Shared Application session.
            This is useful if you want to modify data on the venue without any other
            participant being able to edit this data at the same time.
        """
        
        # Not implemented yet.
        pass
    
    
    def ReleaseLockForConfereceSession(self):
        """
            This method releases the lock for the Shared Application session.
        """
        
        # Not implemented yet.
        pass
        
        
    def StartUpViewer(self, framenumber, filename):
        """
            This method creates a new OSGFrame and its associated subcontroller.
            
            Parameters:
                framenumber: The number of the OSGFrame (according to the list of numbers
                    on the venue's Application Service).
                filename: Local filename of the model to be shown within the new window. 
        """
        subcontroller = SubController( self, framenumber )
        self.subcontrollers[framenumber] = subcontroller
        frame = OSGFrame( subcontroller, filename, 
                          self.mainframe, wx.ID_ANY, 'SharedOSG v2 Viewer' )
        subcontroller.RegisterFrameAndStartRunning( frame )
        
    def CloseFrame(self, framenumber):
        """
            This method is called if one of the OSGFrames are closed.
            It removes the number of this frame from the Application Service and then tells
            all participants to close this window.
            
            Parameters:
                framenumber: The number of the OSGFrame (according to the list of numbers
                    on the venue's Application Service).
        """
        
        # Get a lock so that no one else can edit the sensitive data.
        self.GetLockForConfereceSession()
        
        pickledData = self.sharedAppClient.GetData( 'ActiveFrames' )
        if pickledData != '':
            activeFiles = cPickle.loads( str(pickledData) )
            if framenumber in activeFiles:
                activeFiles.remove(framenumber)
                self.sharedAppClient.SetData( 'ActiveFrames', cPickle.dumps(activeFiles) )
                self.sharedAppClient.SetData( 'File' + str(framenumber), '' )
                self.sharedAppClient.SendEvent('FrameClose' + str(framenumber), '')
        
        self.ReleaseLockForConfereceSession()
        
    def ClosedFrame(self, framenumber):
        """
            This method is called by a subcontroller after it destroyed its OSGFrame.
            It removes the subcontroller from the maincontroller's list of subcontrollers.
            
            Parameters:
                framenumber: The number of the OSGFrame (according to the list of numbers
                    on the venue's Application Service).
        """
        if framenumber in self.subcontrollers.keys():
            self.subcontrollers.pop(framenumber)

        


# Starting the plugin        
if __name__ == '__main__':
    from optparse import Option

    wxapp = wx.PySimpleApp()
    app = WXGUIApplication()

    # Get the application URL and the venue URL from the parameters.
    appUrlOption = Option( "-a", "--applicationURL", dest = "appUrl", default = None,
                           help = 'Specify an Application URL' )
    venueUrlOption = Option( '-v', '--venueURL', dest = 'venueUrl', default = None,
                             help='Specify a Venue URL' )
    app.AddCmdLineOption(appUrlOption)
    app.AddCmdLineOption(venueUrlOption)
    app.Initialize('SharedOSG v2')
    appUrl = app.GetOption('appUrl')
    venueUrl = app.GetOption('venueUrl')


    # If one of the URLs is missing the program cannot start.
    if not appUrl or not venueUrl:
        sys.exit(-1)
       
    MainController.maincontroller = MainController( appUrl, venueUrl )

    
#    wxapp.SetTopWindow(frame)

    
    from twisted.internet import reactor
    reactor.registerWxApp(wxapp)
    reactor.run()
